/**
 * Copyright 2002 Cordys R&D B.V. 
 * 
 * This file is part of the Cordys .NET Connector. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 /**
 * @author  owarnier
 * changed by pkavuri
 */

package com.eibus.applicationconnector.ejb;


import com.eibus.applicationconnector.ejb.localization.message.Messages;
import com.eibus.localization.IStringResource;
import com.eibus.soap.ApplicationTransaction;
import com.eibus.soap.BodyBlock;
import com.eibus.soap.MethodDefinition;
import com.eibus.soap.SOAPTransaction;
import com.eibus.soap.fault.Fault;
import com.eibus.soap.fault.FaultDetail;
import com.eibus.util.guid.Guid;
import com.eibus.util.logger.CordysLogger;

import com.eibus.xml.nom.Document;

import com.eibus.xml.nom.Node;
import com.eibus.xml.xpath.XPath;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.xml.namespace.QName;

import com.eibus.applicationconnector.ejb.localization.message.Messages;

// To avoid pointer tampering - Bug Fix for #42032  - pkavuri.
class EJBTransaction implements ApplicationTransaction {
    private SOAPTransaction soapTransaction;
    private EJBExecutionEnvironment ejbExecutionEnvironment;
    private static final String BEAN_ID="beanid";
    private static final String STR_METHOD="method";
    private boolean ismetaDataMethodFlag = false;
	//	To avoid pointer tampering - Bug Fix for #42032  - pkavuri.
    private static Document doc = new Document();
    static CordysLogger ejbLogger = CordysLogger.getCordysLogger(EJBTransaction.class);
    
    /*Static xpath Objects*/
    private static String beanIdXpath= ".//beanid";
    private static String beanNameXpath= ".//beanname";
    private static String interfaceNameXpath= ".//interfacename";
    public static String parametersXpath= ".//parameters";
    private static final  String DEFAULT_NAME_SPACE = "http://schemas.cordys.com/1.0/metadata/ejb";
    static final QName DEFAULT_QNAME = new QName(DEFAULT_NAME_SPACE,"customFaultDetail");
    private BodyBlock requestBodyBlock;
    
    
    // A constructor.
    EJBTransaction(SOAPTransaction soapTransaction,  EJBExecutionEnvironment ejbExecutionEnvironment) {
        this.soapTransaction = soapTransaction;
        this.ejbExecutionEnvironment = ejbExecutionEnvironment;
        
    }

    // abort method
    public void abort() {
    }

    // Commit method.
    public void commit() {
    }

    // Decides whether request can be processed or not.
    public boolean canProcess(String type) {
        return "EJB".equals(type);
    }

    /**
     * This method will process the requests of this kind and metadata requests.
     * <implementation type="EJB">
     * <bean name="bean-name">
     * <home>
     * <method/>
     * <parameters>
     * <parametername  dt="datatype/>
     * </parameters>
     * </home>
     * <remote>
     * [<beanid>]
     * <method/>
     * <parameters>
     * <parametername  dt="datatype/>
     * </parameters>
     * </remote>
     * </bean>
     * </implementation>
     */
    public boolean process(BodyBlock requestBodyBlock, BodyBlock responseBodyBlock) {
    	this.requestBodyBlock = requestBodyBlock;
        int request =requestBodyBlock.getXMLNode();
        int response = responseBodyBlock.getXMLNode();
        String methodName = Node.getLocalName(request);
        if (ejbLogger.isInfoEnabled())
        	ejbLogger.info("Request received to EJBCall Service is .. \n"+Node.writeToString(request,true));
        try {
           boolean isMetaMethod = handleMetaDataMethods(methodName, request, response); // Handling metadata requests.
           if (isMetaMethod)
           {
        	   if (ejbLogger.isDebugEnabled())
       		{
       			ejbLogger.debug("isMetaMethod returned true");
       			ejbLogger.debug("Response from EJBCall Service is ..\n"+Node.writeToString(response,true));
       		}
        	   return true;
           }
           else
           {
        	   ejbLogger.debug("isMetaMethod returned false");
        		if (ejbLogger.isDebugEnabled())
        		{
        			ejbLogger.debug("isMetaMethod returned false");
        			ejbLogger.debug("Response from EJBCall Service is ..\n"+Node.writeToString(response,true));
        		}

        	   
           }
        }
        catch(EJBExecutionEnvironmentException e) {
        	//responseBodyBlock.createSOAPFault("Server.WrongImplementation", e.getMessage());
           	createSoapFault(responseBodyBlock,e);
        	
        	return false;
        }
		// int implementation = requestBodyBlock.getDefinition().getMethodImplementation();
		int implementation =0;
		implementation = requestBodyBlock.getMethodDefinition().getImplementation();
		try {
	        // handleBeanCalls(request, response, duplicatedImplementation);
			handleBeanCalls(request, response, implementation);
		
		}
		catch(EJBExecutionEnvironmentException e) {
			// e.printStackTrace();
			//responseBodyBlock.createSOAPFault("Server.WrongImplementation", e.getMessage());
			createSoapFault(responseBodyBlock,e);

			return false;
		}
		if (ejbLogger.isInfoEnabled())
			ejbLogger.info("Response from EJBCall Service is ..\n"+Node.writeToString(response,true));
        return true;
    }

	private void createSoapFault(BodyBlock responseBodyBlock,EJBExecutionEnvironmentException e)
	{
		Fault fault =null;
    	if(e.getIstringResource() == null)
    	{
    		fault = responseBodyBlock.createSOAPFault(Fault.Codes.SERVER,Messages.GENERAL_EXCEPTION);
    		//addFaultDetails(fault.getDetail(),responseBodyBlock,null,null,e);
    		addFaultDetails2(fault.getDetail(),e,DEFAULT_QNAME,null,null);
    	}
    	else
    	{
    		if(e.getArguments() == null)
    		{
    			fault = responseBodyBlock.createSOAPFault(Fault.Codes.SERVER,e.getIstringResource());
    		}
    		else
    		{
    			fault = responseBodyBlock.createSOAPFault(Fault.Codes.SERVER,e.getIstringResource(),e.getArguments());
    		}
    		//addFaultDetails(fault.getDetail(),responseBodyBlock,null,null,e);
    		addFaultDetails2(fault.getDetail(),e,DEFAULT_QNAME,null,null);
       	}
		
	}

	private void handleBeanCalls(int request, int response, int implementation)
				throws EJBExecutionEnvironmentException {
		EJBCall ejbCall = null;
		//int beanDefinition = Find.firstMatch(implementation, "<implementation><bean>");
		int beanDefinition = Node.getElement(implementation,"bean");
		//int homeNode = Find.firstMatch(implementation,"<implementation><bean><home>");
		int homeNode = Node.getElement(beanDefinition,"home");
		//int remoteNode = Find.firstMatch(implementation,"<implementation><bean><remote>");
		int remoteNode = Node.getElement(beanDefinition,"remote");
		//int beanIDNode = Find.firstMatch(request, "?<beanid>"); // .//beanid
		//int beanIDNode = beanIdXpath.firstMatch(request,null);
		int beanIDNode = XPath.getFirstMatch(beanIdXpath, null, request) ;
		String beanId = null;
		Guid beanID = null;
		int tupleNode = doc.createElementWithParentNS("tuple",null, response);
		int oldTagNode = doc.createElementWithParentNS("old",null,tupleNode);
		if (beanIDNode != 0) {
			beanId = Node.getDataWithDefault(beanIDNode,BEAN_ID);
			ejbCall = getEJBCallById(beanId);	// if bean ID exists in the request
		}
		else if (null == ejbCall) {
			ejbCall = getEJBCallByName(beanDefinition); //	Execute the Home Interface create method.
			beanID = createEJBObject(homeNode, remoteNode, request, ejbCall); // Create EJBObject
			if (remoteNode == 0) {
				 String homeMethod = Node.getDataElement(homeNode, STR_METHOD, "");
				 int beanNameNode = doc.createElementWithParentNS(homeMethod, null,oldTagNode);
				 doc.createElementWithParentNS(BEAN_ID, beanID.toString(), beanNameNode);
			}
		}
		// Execute Remote Method's Business method.
		if (remoteNode!=0){
			Object returnObject = executeRemoteMethod(remoteNode, request, ejbCall);
			if (beanID != null)
				doc.createTextElement(BEAN_ID, beanID.toString(), oldTagNode);
			else
				doc.createTextElement(BEAN_ID, beanId , oldTagNode);
			String beanMethod = Node.getDataElement(remoteNode, STR_METHOD, "");
			int beanNameNode = doc.createElementWithParentNS(beanMethod,null, oldTagNode);
			if (null != returnObject)
				doc.createText(returnObject.toString(), beanNameNode);
		}
	}

    private EJBCall getEJBCallById(String beanId)
    			throws EJBExecutionEnvironmentException {
		EJBCall  ejbCall = null;
		try {
				Guid beanID = Guid.createGuid(beanId);
				if (ejbLogger.isDebugEnabled())
					ejbLogger.debug("Bean ID is" + beanID);
				ejbCall = ejbExecutionEnvironment.getEJBCall(beanID);
		}
		catch(Exception e) {
			throw new EJBExecutionEnvironmentException("BeanId is not correct." + e.getMessage(),Messages.BEAN_ID_INCORRECT,new String[]{beanId});
		}
		return ejbCall;
    }

    private EJBCall getEJBCallByName(int beanDefinition)
					throws EJBExecutionEnvironmentException {
		String beanName= Node.getAttribute(beanDefinition, "name","");
		if (beanName == null || beanName.trim().length()==0)
			throw new EJBExecutionEnvironmentException("JNDI Identifier of bean is not specified",Messages.JNDI_ID_NOT_SPECIFIED);

		if (ejbLogger.isDebugEnabled())
			ejbLogger.debug("Creating call for bean "+beanName);
		
		String jndiName = Node.getDataElement(beanDefinition, "jndiname", beanName);
		ejbExecutionEnvironment.getMetaData(beanName).setEjbJndiName(jndiName);
		return ejbExecutionEnvironment.startCall(beanName);
    }

    private Guid createEJBObject(int homeNode, int remoteNode, int request, EJBCall ejbCall)
    					throws EJBExecutionEnvironmentException {
		Guid beanID = null;
		try {
			String homeMethod = Node.getDataElement(homeNode, STR_METHOD, "");
			if (homeMethod.trim().length()==0)
				throw new EJBExecutionEnvironmentException("Home Method is not specified",Messages.HOME_METHOD_NOT_SPECIFIED);
			if (ejbLogger.isDebugEnabled())
				ejbLogger.debug("Calling Home interfaces " + homeMethod + STR_METHOD);
			ParameterHandler params = null;
			if (remoteNode == 0) // Read from the request
				params = getParameters(homeNode, request);
			else // read from the implementation
				params = getParameters(homeNode, homeNode);
			ejbCall.createRemoteObject(homeMethod, params.getValues(), params.getTypes());
			beanID = Guid.createGuid();
			ejbExecutionEnvironment.addEJBCall(beanID, ejbCall);
		}
		catch (EJBCallException ecEx) {
			throw new EJBExecutionEnvironmentException("EJBCall failed: " + ecEx.getMessage(),Messages.EJB_CALLFAILED);
		}
		catch (ArgumentException ecEx) {
			throw new EJBExecutionEnvironmentException("Improper Arguments -" + ecEx.getMessage(),Messages.IMPROPER_ARGUMENTS);
		}
		return beanID;
    }

    private Object executeRemoteMethod(int remoteNode, int request, EJBCall ejbCall)
								throws EJBExecutionEnvironmentException {
	   Object returnObject = null;
	   try {
		   String beanMethod = Node.getDataElement(remoteNode, STR_METHOD, "");
		   if (beanMethod.trim().length()==0)
			   throw new EJBExecutionEnvironmentException("Remote Method is not specified",Messages.REMOTE_METHOD_NOT_SPECIFIED);
		   if (ejbLogger.isDebugEnabled())
		   	ejbLogger.debug("Calling Remote interfaces " + beanMethod + STR_METHOD);
		   ParameterHandler params = getParameters(remoteNode,request);
		   returnObject = ejbCall.invokeRemote(beanMethod, params.getValues(), params.getTypes());
	   }
	   catch (EJBCallException ecEx) {
		   throw new EJBExecutionEnvironmentException("EJBCall failed: " + ecEx.getMessage(),Messages.EJB_CALLFAILED);
	   }
	   catch (ArgumentException ecEx) {
			throw new EJBExecutionEnvironmentException("Improper Arguments - " + ecEx.getMessage(),Messages.IMPROPER_ARGUMENTS);
	   }
	   return returnObject;
	}


    // Method will handle the the parameters
    private ParameterHandler getParameters(int node, int request)throws ArgumentException {
        return new ParameterHandler(node, request);
    }

    // method handles the metadatamethods such as getEntityBeans, getsSessionBeans, getBeanInfo and getInterfaceInfo
    // methods.
    private boolean handleMetaDataMethods(String methodName, int request, int response) throws EJBExecutionEnvironmentException{
    	boolean isMetaMethod = true;
        if ("getSessionBeans".equals(methodName)){
            if (ejbLogger.isDebugEnabled())
            	ejbLogger.debug("Calling getSessionBeans() Method");
            ejbExecutionEnvironment.getBeans("session", response);
        }
        else if ("getEntityBeans".equals(methodName)){
        	if (ejbLogger.isDebugEnabled())
        		ejbLogger.debug("Calling getEntityBeans() Method");
                
            ejbExecutionEnvironment.getBeans("entity", response);
        }
        else if ("getBeanInfo".equals(methodName)){
            if (ejbLogger.isDebugEnabled())
            	ejbLogger.debug("Calling getBeanInfo() Method");
            //String beanName = Node.getData(Find.firstMatch(request,"?<beanname>")); // .//beanname
            //String beanName = Node.getData(beanNameXpath.firstMatch(request,null));
            String beanName = Node.getData(XPath.getFirstMatch(beanNameXpath, null, request));
            
            ejbExecutionEnvironment.getBeanInfo(beanName, response);
        }
        else if ("getInterfaceInfo".equals(methodName)) {
            if (ejbLogger.isDebugEnabled())
            	ejbLogger.debug("Calling getInterfaceInfo() Method");
            String className = null;
			//String beanName = Node.getData(Find.firstMatch(request,"?<beanname>"));
			//String beanName = Node.getData(beanNameXpath.firstMatch(request,null));
            String beanName = Node.getData(XPath.getFirstMatch(beanNameXpath, null, request));
           // className = Node.getData(Find.firstMatch(request,"?<interfacename>"));
            //className = Node.getData(interfaceNameXpath.firstMatch(request,null));
            className = Node.getData(XPath.getFirstMatch(interfaceNameXpath, null, request));
            ejbExecutionEnvironment.getInterfaceInfo(beanName, className, response);
        }
        else
         	isMetaMethod = false;
        return isMetaMethod;
    }    
   
    
    protected static  void addFaultDetails2(FaultDetail faultDetail,Throwable exception,QName qname,IStringResource details,Object[] detailInsertionArguments)
    {                       
    	if(details != null)
    	{
    		faultDetail.addDetailEntry(qname, details, detailInsertionArguments);
    	}
    	
       if(exception != null)
       {
    	   faultDetail.addDetailEntry(exception);
       }                         
    }
    
    /**
	 * Return the Name space Definition.
	 * @return
	 */	
	protected String  getNameSpace() 
	{
		MethodDefinition definition = requestBodyBlock.getMethodDefinition();
		if(definition != null)
		{
			return definition.getNamespace();
		}
		return DEFAULT_NAME_SPACE;
			
	}
}

/**
 * A helper class which handles and matches the list of parameters of request and
 * implementation.
 */
class ParameterHandler {
    private Class[] parameterTypes;
    private Object[] parameterValues;
    CordysLogger ejbLogger = CordysLogger.getCordysLogger(EJBTransaction.class);
    // return all the parameter values.
    public Object[] getValues() {
        return parameterValues;
    }

    // return the type of parameters.
    public Class[] getTypes() {
        return parameterTypes;
    }

    ParameterHandler(int node, int request) throws ArgumentException {
        
    	//int parametersNode = Find.firstMatch(node, "?<parameters>");
    	//int parametersNode = EJBTransaction.parametersXpath.firstMatch(node,null);
    	int parametersNode = XPath.getFirstMatch(EJBTransaction.parametersXpath, null, node);
        int parameterNode = Node.getFirstChildElement(parametersNode);
        int numParameters = Node.getNumChildElements(parametersNode);
        if (numParameters > 0) {
            parameterTypes = new Class[numParameters];
            parameterValues = new Object[numParameters];
        }
        else {
        	if (ejbLogger.isDebugEnabled())
        		ejbLogger.debug( "method has no parameters");
       
        }
        for (int i=0; i<numParameters; i++) {
            String xmlType = Node.getAttribute(parameterNode, "dt", "string");
            //int argNode = Find.firstMatch(request, "?<"+ Node.getName(parameterNode).trim()+">");
            String argXpath = ".//"+ Node.getName(parameterNode).trim();
            //int argNode = argXpath.firstMatch(request,null);
            int argNode = XPath.getFirstMatch(argXpath, null, request) ;
            if (argNode !=0 ) {
                Parameter param = new Parameter(argNode, xmlType);
                parameterTypes[i] = param.getType();
                parameterValues[i] = param.getValue();
                parameterNode = Node.getNextElement(parameterNode);
            }
            else
                throw new ArgumentException("Invalid Request. Method definition is not matched with Request.");
        }
    }

    ParameterHandler() 
    {
        parameterTypes = null;
        parameterValues = null;
    }
    
   
}

class Parameter {
    private static SimpleDateFormat simpleDateFormat; // Used to do Date from/to String conversions
    private int node;
    private Class type;
    private Object value;
    public static final String numberFormatExceptionMsg = "NumberFormatException : parameter initialised to zero" ;
    static {
        simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        simpleDateFormat.setTimeZone(new java.util.SimpleTimeZone(0,"GMT"));
    }
    CordysLogger ejbLogger = CordysLogger.getCordysLogger(EJBTransaction.class);

    Parameter(int node, String xmlType)  throws ArgumentException {
        this.node = node;
        if ("string".equals(xmlType))
            initString();
        else if ("int".equals(xmlType) || "i4".equals(xmlType) || "ui2".equals(xmlType))
            initInteger();
        else if ("boolean".equals(xmlType))
            initBoolean();
        else if ("number".equals(xmlType))
            initBigDecimal();
        else if ("ui8".equals(xmlType))
            initBigInteger();
        else if ("i1".equals(xmlType))
            initByte();
        else if ("dateTime".equals(xmlType))
            initDate();
        else if ("r8".equals(xmlType))
            initDouble();
        else if ("r4".equals(xmlType))
            initFloat();
        else if ("i2".equals(xmlType) || "ui1".equals(xmlType))
            initShort();
        else if ("ui4".equals(xmlType) || "i8".equals(xmlType))
            initLong();
        else
            initString();
    }

    /** Returns the Java Class of the Parameter (i.e. the type) */
    Class getType() {
        return type;
    }

    /** Returns the value of the parameter 	 */
    Object getValue() {
        return value;
    }

    /** The following functions interpret the XML version
     * of the parameter as specific Java Objects and their Classes
     */
    private void initString() {
        type = String.class;
        value = Node.getDataWithDefault(node, null);
    }

    private void initInteger() throws ArgumentException {
        type = int.class;
        try {
            value = new Integer(Node.getDataWithDefault(node,"0"));
        }
        catch (NumberFormatException nfe) {
            value = new Integer(0);
            if (ejbLogger.isWarningEnabled())
            	ejbLogger.warn(numberFormatExceptionMsg);
            throw new ArgumentException(nfe.getMessage());
        }
    }

    private void initBigInteger() throws ArgumentException{
        type = BigInteger.class;
        try {
            value = new BigInteger(Node.getDataWithDefault(node, "0"));
        }
        catch (NumberFormatException nfe) {
            value = new BigInteger("0");
            if (ejbLogger.isWarningEnabled())
            	ejbLogger.warn(numberFormatExceptionMsg);
       
            throw new ArgumentException(nfe.getMessage());
        }
    }

    private void initBigDecimal()  throws ArgumentException {
        type = BigDecimal.class;
        try {
            value = new BigDecimal(Node.getDataWithDefault(node, "0"));
        }
        catch (NumberFormatException nfe) {
            value = new BigDecimal("0");
            if (ejbLogger.isWarningEnabled())
            	ejbLogger.warn(numberFormatExceptionMsg);
       
            throw new ArgumentException(nfe.getMessage());
        }
    }
    private void initLong()   throws ArgumentException{
        type = long.class;
        try {
            value = new Long(Node.getDataWithDefault(node,"0"));
        }
        catch (NumberFormatException nfe) {
            value = new Long((long)0);
            if (ejbLogger.isWarningEnabled())
            	ejbLogger.warn(numberFormatExceptionMsg);
       
            throw new ArgumentException(nfe.getMessage());
        }
    }
    private void initShort()  throws ArgumentException{
        type = short.class;
        try {
            value = new Short(Node.getDataWithDefault(node,"0"));
        }
        catch (NumberFormatException nfe) {
            value = new Short((short)0);
            if (ejbLogger.isWarningEnabled())
            	ejbLogger.warn(numberFormatExceptionMsg);
       
            throw new ArgumentException(nfe.getMessage());
        }
    }
    private void initByte()  throws ArgumentException {
        type = byte.class;
        try {
            value = new Byte(Node.getDataWithDefault(node,"0"));
        }
        catch (NumberFormatException nfe) {
            value = new Byte((byte) 0);
            if (ejbLogger.isWarningEnabled())
            	ejbLogger.warn(numberFormatExceptionMsg);
       
            throw new ArgumentException(nfe.getMessage());
        }
    }
    private void initBoolean() {
        type = boolean.class;
        value = new Boolean(Node.getDataWithDefault(node,"false"));
    }
    private void initDate()  throws ArgumentException {
        type = Date.class;
        try {
        	synchronized(this)
        	{
        		value = simpleDateFormat.parse(Node.getDataWithDefault(node,null));
        	}
        }
        catch (Exception e) {
            value = null;
            throw new ArgumentException(e.getMessage());
        }
    }

    private void initDouble()  throws ArgumentException {
        type = double.class;
        try {
            value = new Double(Node.getDataWithDefault(node, "0"));
        }
        catch (NumberFormatException nfe) {
            value = new Double((double) 0);
            if (ejbLogger.isWarningEnabled())
            	ejbLogger.warn(numberFormatExceptionMsg);
       
            throw new ArgumentException(nfe.getMessage());
        }
    }

    private void initFloat() throws ArgumentException {
        type = float.class;
        try {
            value = new Float(Node.getDataWithDefault(node,"0"));
        }
        catch (NumberFormatException nfe) {
            value = new Float((float) 0);
            if (ejbLogger.isWarningEnabled())
            	ejbLogger.warn(numberFormatExceptionMsg);
       
            throw new ArgumentException(nfe.getMessage());
        }
    }

    public String toString() {
        return "["+getType()+"="+getValue()+"]";
    }
    
    
   
}
